// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROMECAST_CAST_CORE_GRPC_GRPC_UNARY_CALL_H_
#define CHROMECAST_CAST_CORE_GRPC_GRPC_UNARY_CALL_H_

#include <grpcpp/grpcpp.h>
#include <grpcpp/support/client_callback.h>

#include "base/bind.h"
#include "base/callback.h"
#include "chromecast/cast_core/grpc/grpc_call.h"
#include "chromecast/cast_core/grpc/grpc_client_reactor.h"
#include "chromecast/cast_core/grpc/grpc_status_or.h"

namespace cast {
namespace utils {

// Typedef for the unary sync method generated by gRPC compiler.
template <typename TSyncInterface, typename TRequest, typename TResponse>
using SyncUnaryMethod = grpc::Status (TSyncInterface::*)(grpc::ClientContext*,
                                                         const TRequest&,
                                                         TResponse*);

// Typedef for the unary async method generated by gRPC compiler.
template <typename TAsyncInterface, typename TRequest, typename TResponse>
using AsyncUnaryMethod = void (TAsyncInterface::*)(grpc::ClientContext*,
                                                   const TRequest*,
                                                   TResponse*,
                                                   grpc::ClientUnaryReactor*);

// A GrpcCall implementation for unary gRPC calls specialized by the
// |AsyncMethodPtr| and SyncMethodPtr function pointers. The former must always
// be specified and is used for Async calls. The latter is used for Sync calls
// and could be omitted.
//  TGrpcStub - gRPC service stub type.
//  TRequest - gRPC request type for a method in the stub.
//  TResponse - gRPC response type for a method in the stub.
//  AsyncMethodPtr - pointer to an async method in the stub that handles a
//  streaming call.
//  SyncMethodPtr - pointer to a sync method in the stub that handles a
//  streaming call.
template <
    typename TGrpcStub,
    typename TRequest,
    typename TResponse,
    AsyncUnaryMethod<typename TGrpcStub::AsyncInterface, TRequest, TResponse>
        AsyncMethodPtr,
    SyncUnaryMethod<typename TGrpcStub::SyncInterface, TRequest, TResponse>
        SyncMethodPtr = nullptr>
class GrpcUnaryCall : public GrpcCall<TGrpcStub, TRequest> {
 public:
  static_assert(AsyncMethodPtr != nullptr, "AsyncMethodPtr must be specified");

  using Base = GrpcCall<TGrpcStub, TRequest>;
  using Base::async;
  using Base::GrpcCall;
  using Base::request;
  using Base::sync;
  using typename Base::AsyncInterface;
  using typename Base::Request;

  using Response = TResponse;
  using ResponseCallback = base::OnceCallback<void(GrpcStatusOr<Response>)>;

  GrpcStatusOr<Response> Invoke() && {
    static_assert(SyncMethodPtr != nullptr,
                  "The sync interface is not defined for the stub. Please, add "
                  "&StubInterface::<method_name> to the GrpcUnaryCall "
                  "definition in the stub class.");
    Response response;
    grpc::ClientContext context;
    std::move(*this).options().ApplyOptionsToContext(&context);
    auto call = base::BindOnce(SyncMethodPtr,
                               base::Unretained(std::move(*this).sync()));
    auto status =
        std::move(call).Run(&context, std::move(*this).request(), &response);
    if (status.ok()) {
      return response;
    }
    return status;
  }

  // Invokes a unary gRPC call for a given tuple of AsyncInterface,
  // AsyncMethodPtr, Request and Response. The |service| must have a |method| of
  // a certain signature generated by the proto compiler.
  void InvokeAsync(ResponseCallback response_callback) && {
    // Although |reactor| is a plain pointer, its ownership is transferred to
    // the gRPC stack in the |Start| call.
    auto reactor =
        new Reactor(std::move(*this).async(), std::move(*this).request(),
                    std::move(*this).options(), std::move(response_callback));
    reactor->Start();
  }

 private:
  using ReactorBase = GrpcClientReactor<Request, grpc::ClientUnaryReactor>;

  class Reactor final : public ReactorBase {
   public:
    using ReactorBase::context;
    using ReactorBase::request;

    Reactor(AsyncInterface* async_stub,
            Request request,
            GrpcCallOptions options,
            ResponseCallback response_callback)
        : ReactorBase(std::move(request), std::move(options)),
          async_stub_call_(
              base::BindOnce(AsyncMethodPtr, base::Unretained(async_stub))),
          response_callback_(std::move(response_callback)) {}

    void Start() override {
      ReactorBase::Start();
      std::move(async_stub_call_).Run(context(), request(), &response_, this);
      grpc::ClientUnaryReactor::StartCall();
    }

   private:
    // Implements grpc::ClientUnaryReactor APIs.
    // The method is always called on completion of all operations associated
    // with this call, and deletes itself on exit.
    void OnDone(const grpc::Status& status) override {
      if (status.ok()) {
        std::move(response_callback_).Run(std::move(response_));
      } else {
        std::move(response_callback_).Run(status);
      }
      delete this;
    }

    using AsyncStubCall = base::OnceCallback<void(grpc::ClientContext*,
                                                  const Request*,
                                                  Response*,
                                                  grpc::ClientUnaryReactor*)>;

    AsyncStubCall async_stub_call_;
    ResponseCallback response_callback_;
    Response response_;
  };
};

}  // namespace utils
}  // namespace cast

#endif  // CHROMECAST_CAST_CORE_GRPC_GRPC_UNARY_CALL_H_
